using System;
using System.Xml;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace Dida.Dxml
{
	public class ObjectConstructedArgs : EventArgs
	{
		XmlCompiler compiler;
		object obj;

		public ObjectConstructedArgs(XmlCompiler compiler, object obj)
		{
			this.compiler = compiler;
			this.obj = obj;
		}

		public XmlCompiler Compiler
		{
			get { return compiler; }
		}

		public object Object
		{
			get { return obj; }
		}
	}

	public delegate void ObjectConstructedHandler(object sender, ObjectConstructedArgs args);

	public class CompilationFinishedArgs : EventArgs
	{
		XmlCompiler compiler;
		ObjectManager objManager;

		public CompilationFinishedArgs(XmlCompiler compiler, ObjectManager objManager)
		{
			this.compiler = compiler;
			this.objManager = objManager;
		}

		public XmlCompiler Compiler
		{
			get { return compiler; }
		}

		public ObjectManager ObjectManager
		{
			get { return objManager; }
		}
	}

	public delegate void CompilationFinishedHandler(object sender,CompilationFinishedArgs args);

	public class XmlCompiler
	{
		List<CompilingObject> objects = new List<CompilingObject>();
		List<Namespace> namespacesStack = new List<Namespace>();

		List<IContainerObjectCompiler> containersStack = 
			new List<IContainerObjectCompiler>();

		Dictionary<IContainerObjectCompiler, CompilingObject> globalContainersObjects = 
			new Dictionary<IContainerObjectCompiler, CompilingObject>();

		IExternalsLoader externalsLoader = null;

		ObjectManager result;

		public XmlCompiler()
		{
			Namespace rootNamespace = new Namespace(null,null);
			namespacesStack.Add(rootNamespace);
		}

		public void Load(XmlReader reader, IExternalsLoader externalsLoader)
		{
			Debug.Assert(reader.Settings.IgnoreComments, "The IgnoreComments property of the XmlReader's settings must be true");
			Debug.Assert(reader.Settings.IgnoreWhitespace, "The IgnoreWhitespace property of the XmlReader's settings must be true");

			this.externalsLoader = externalsLoader;

			reader.MoveToContent();
			ProcessElement(reader);
		}

		public void Load(string xmlFileName)
		{
			XmlReaderSettings settings = new XmlReaderSettings();
			settings.IgnoreComments = true;
			settings.IgnoreWhitespace = true;
			XmlReader reader = XmlReader.Create(xmlFileName, settings);

			FileInfo fileInfo = new FileInfo(xmlFileName);
			ExternalFileLoader fileLoader = new ExternalFileLoader(fileInfo.DirectoryName);

			Load(reader,fileLoader);
		}

		public void LoadContainerChildren(XmlReader reader, IContainerObjectCompiler container)
		{
			containersStack.Add(container);

			if (reader.IsEmptyElement)
				reader.ReadStartElement();
			else
			{
				reader.ReadStartElement();
				while (reader.NodeType != XmlNodeType.EndElement)
				{
					ProcessElement(reader);
				}
				reader.ReadEndElement();
			}

			containersStack.RemoveAt(containersStack.Count - 1);
		}

		private void ProcessElement(XmlReader reader)
		{
			switch (reader.Name)
			{
				case "Group":
					ProcessGroup(reader);
					break;

				case "Import":
					ProcessImport(reader);
					break;

				default:
					ProcessObjectElement(reader);
					break;
			}
		}

		private void ProcessGroup(XmlReader reader)
		{
			if (reader.IsEmptyElement)
				return;

			int nNamespaces = 0;

			string nsStr = reader.GetAttribute("namespace");
			if (nsStr != null)
			{
				nsStr = nsStr.Trim();
				if (nsStr != "")
				{
					if (nsStr[0] == '.')
					{
						throw new XmlAttributeException("A namespace name shouldn't start " +
							"with a '.' (dot).", reader, "namespace");
					}


					XmlPositionMarker namespaceAttrMarker = XmlPositionMarker.GetAttrPosition(reader,"namespace");
					string[] namespaces = nsStr.Split(".".ToCharArray());
					foreach (string nsName in namespaces)
					{
						Namespace newNs = new Namespace(CurNamespace,null);
						newNs.SetName(nsName,namespaceAttrMarker);
						PushNamespace(newNs);
						nNamespaces++;
					}
				}
			}

			reader.ReadStartElement();
			while (reader.NodeType != XmlNodeType.EndElement)
			{
				ProcessElement(reader);
			}
			reader.ReadEndElement();

			PopNamespaces(nNamespaces);
		}

		private void ProcessImport(XmlReader reader)
		{
			string src = reader.GetAttribute("src");
			if (src == null)
				throw new Exception("No src attribute specified to the import tag.");

			IExternalStream stream = externalsLoader.BeginStreamBlock(src,new XmlPositionMarker(reader));

			XmlReader importedReader = XmlReader.Create(stream.OpenStream());
			importedReader.MoveToContent();
			ProcessElement(importedReader);

			if (reader.IsEmptyElement)
				reader.ReadStartElement();
			else
			{
				reader.ReadStartElement();
				reader.ReadEndElement();
			}

			externalsLoader.EndStreamBlock(stream);
		}

		private void ProcessObjectElement(XmlReader reader)
		{
			string tagName = reader.Name;
			Type type = DidaXml.GetObjectCompilerType(tagName);

			IXmlObjectCompiler curObjCompiler = (IXmlObjectCompiler)Activator.CreateInstance(type);
			CompilingObject compilingObj = new CompilingObject(curObjCompiler, 
				new XmlPositionMarker(reader));
			
			Namespace objNamespace = new Namespace(CurNamespace,compilingObj);
			compilingObj.ChildsNamespace = objNamespace;
			PushNamespace(objNamespace);

			curObjCompiler.Load(reader, this);
			
			PopNamespaces(1);
			// TODO: Error message should be at name attr.
			objNamespace.SetName(curObjCompiler.Name,new XmlPositionMarker(reader));

			objects.Add(compilingObj);

			for(int i = containersStack.Count - 1;i >= 0;i--)
			{
				IContainerObjectCompiler container = containersStack[containersStack.Count - 1];
				ContainerAddResult addRes = container.AddChild(compilingObj);
				if (addRes == ContainerAddResult.Added || addRes == ContainerAddResult.NotAddedBlocked)
					break;
			}
		}

		public void AddGlobalContainer(IContainerObjectCompiler container)
		{
			containersStack.Add(container);

			CompilingObject obj = new CompilingObject(container,null);
			obj.ChildsNamespace = namespacesStack[namespacesStack.Count - 1];
			objects.Add(obj);

			globalContainersObjects.Add(container, obj);
		}

		public int GetGlobalContainerIndex(IContainerObjectCompiler container)
		{
			return globalContainersObjects[container].Index;
		}

		public object GetGlobalContainerObj(IContainerObjectCompiler container)
		{
			return result.GetObject(globalContainersObjects[container].Index);
		}

		internal CompilingObject GetCompilingObject(string name,IList<string> nameParts,
			Namespace declarationNamespace,XmlPositionMarker errorPos)
		{
			Namespace curNamespace = declarationNamespace;
			while (curNamespace != null)
			{
				CompilingObject obj = curNamespace.GetObject(nameParts, errorPos);
				if (obj != null)
					return obj;
				
				curNamespace = curNamespace.ParentNamespace;
			}

			throw new XmlReaderException("Object not found: \"" + name + "\"",errorPos);
		}

		internal CompilingObject GetCompilingObjectAbsolute(string name, IList<string> nameParts,
			XmlPositionMarker errorPos)
		{
			CompilingObject ret = namespacesStack[0].GetObject(nameParts, errorPos);
			if (ret != null)
				return ret;
			else
			{
				throw new XmlReaderException("Object not found: \"" + name + "\"", errorPos);
			}
		}

		public ObjectManager Compile()
		{
			ResolveDependencies();
			SortDependencies();
			result = ConstructObjects();

			CompilationFinishedHandler handler = CompilationFinished;
			if (handler != null)
				handler(this, new CompilationFinishedArgs(this,result));

			return result;
		}

		private void ResolveDependencies()
		{
			foreach (CompilingObject obj in objects)
				obj.ResolveDependencies(this);
		}

		private void SortDependencies()
		{
			try
			{
				objects = DependencySorter.SortDependencies(objects);

				for (int i = 0; i < objects.Count; i++)
				{
					objects[i].Index = i;
				}
			}
			catch (DependencySorter.CircularReferenceException err)
			{
				throw err;
			}
		}

		private ObjectManager ConstructObjects()
		{
			object[] resObjs = new object[objects.Count];
			Dictionary<string, int> symbols = new Dictionary<string, int>();
			for (int i = 0; i < objects.Count; i++)
			{
				resObjs[i] = objects[i].DoCreateObject(this);

				if (objects[i].Export)
					symbols.Add(objects[i].Name, i);

				// call the ObjectConstructed event.
				ObjectConstructedHandler handler = ObjectConstructed;
				if (handler != null)
					handler(this, new ObjectConstructedArgs(this, resObjs[i]));
			}
			return new ObjectManager(resObjs,symbols);
		}

		private Namespace CurNamespace
		{
			get { return namespacesStack[namespacesStack.Count - 1]; }
		}

		private void PushNamespace(Namespace ns)
		{
			namespacesStack.Add(ns);
		}

		private void PopNamespaces(int num)
		{
			namespacesStack.RemoveRange(namespacesStack.Count - num,num);
		}

		public IExternalsLoader GetExternalsLoader()
		{
			return externalsLoader;
		}

		public event ObjectConstructedHandler ObjectConstructed;
		public event CompilationFinishedHandler CompilationFinished;
	}

	public class CompilingObject : DependencySorter.IItem
	{
		IXmlObjectCompiler objectCompiler;
		Namespace ns;
		XmlPositionMarker errorPos;

		List<CompilingObject> dependentObjects = new List<CompilingObject>();

		object compiled;
		int index = -1;

		internal CompilingObject(IXmlObjectCompiler objectCompiler,XmlPositionMarker errorPos)
		{
			this.objectCompiler = objectCompiler;
			this.errorPos = errorPos;
		}

		public string Name
		{
			get { return objectCompiler.Name; }
		}

		internal int Index
		{
			get { return index; }
			set { index = value; }
		}

		public XmlPositionMarker ErrorPos
		{
			get { return errorPos; }
		}

		/// <summary>
		/// The namespace of this object. This is not the namespace
		/// this object is a child of, it's the namespace this object's children
		/// are a child of, and it's used as a starting point for resolving references
		/// defined in this object.
		/// </summary>
		internal Namespace ChildsNamespace
		{
			get { return ns; }
			set { ns = value; }
		}

		public bool Export
		{
			get { return objectCompiler.Export; }
		}

		public string QualifiedName
		{
			get { return ns.QualifiedName; }
		}

		public IXmlObjectCompiler ObjectCompiler
		{
			get { return objectCompiler; }
		}

		public object DoCreateObject(XmlCompiler compiler)
		{
			if (objectCompiler.ReferencesOrder == ReferencesOrder.DependenciesOrder)
			{
				dependentObjects.Sort(
					delegate(CompilingObject a, CompilingObject b)
					{
						return a.Index.CompareTo(b.Index);
					});
			}

			object[] references = new object[dependentObjects.Count];
			for (int i = 0; i < dependentObjects.Count; i++)
			{
				references[i] = dependentObjects[i].compiled;
				Debug.Assert(references[i] != null);
			}

			compiled = objectCompiler.CreateObject(compiler, references);
			return compiled;
		}

		public void ResolveDependencies(XmlCompiler compiler)
		{
			CompilingReference[] refs = objectCompiler.GetReferences();
			foreach (CompilingReference reference in refs)
			{
				CompilingObject dep;
				if (reference.IsAbsolute)
				{	
					dep = compiler.GetCompilingObjectAbsolute(reference.Name,
						reference.NameParts, reference.ErrorPosition);					
				}
				else
				{
					dep = compiler.GetCompilingObject(reference.Name,
						reference.NameParts, ns, reference.ErrorPosition);
				}

				if (!reference.ReferencedType.IsAssignableFrom(dep.ObjectCompiler.ObjectType))
				{
					throw new IncompatibleReferenceException(reference.Name,
						dep.ObjectCompiler.ObjectType, reference.ReferencedType,reference.ErrorPosition);
				}

				dependentObjects.Add(dep);
			}
		}

		public int CompareTo(object obj)
		{
			return QualifiedName.CompareTo(((CompilingObject)obj).QualifiedName);
		}

		public IList<CompilingObject> Dependencies
		{
			get { return dependentObjects; }
		}

		IList<DependencySorter.IItem> DependencySorter.IItem.Dependencies
		{
			get
			{
				List<DependencySorter.IItem> res = new List<DependencySorter.IItem>();
				foreach (CompilingObject dep in dependentObjects)
					res.Add(dep);
				return res;
			}
		}

		public int CompareTo(DependencySorter.IItem other)
		{
			return QualifiedName.CompareTo(((CompilingObject)other).QualifiedName);
		}

		public CompilingReference Reference
		{
			get
			{
				return CompilingReference.CreateAbsoluteReference(QualifiedName,
					objectCompiler.ObjectType, null);
			}
		}
	}

	internal class Namespace
	{
		string name;
		XmlPositionMarker namePosMarker;		// used when a name related error occurs

		Namespace parentNS;
		Dictionary<string, Namespace> childNamespaces = new Dictionary<string, Namespace>();

		CompilingObject associatedObject = null;

		public Namespace(Namespace parentNS, CompilingObject associatedObject)
		{
			this.name = null;
			this.parentNS = parentNS;
			this.associatedObject = associatedObject;
		}

		/// <summary>
		/// Returns whether this is an object namespace.
		/// </summary>
		/// <remarks>
		/// An object namespace is actually an object which also functions as, a namespace.
		/// io. Child items of this namespace are considered child objects of this object.<br/>
		/// For example, a &lt;list/> object, whichis an object that can 
		/// have child items. To addres a child item of such an object, 
		/// you have to specify the name of the object, followed by dot (.) and
		/// the name of the child object.
		/// <code>
		/// ourList.elementInList
		/// </code>
		/// </remarks>
		public bool IsObjectNamespace
		{
			get { return associatedObject != null; }
		}

		public bool IsNameFree(string name)
		{
			return !childNamespaces.ContainsKey(name);
		}

		public string Name
		{
			get { return name; }
		}

		public void SetName(string newName, XmlPositionMarker errorPosition)
		{
			if (name != null && parentNS != null)
			{
				parentNS.childNamespaces.Remove(name);
			}

			name = newName;
			namePosMarker = errorPosition;

			if (parentNS != null)
			{
				if (parentNS.childNamespaces.ContainsKey(name))
				{
					if (parentNS.name == null)
					{
						throw new XmlReaderException("There's already an item called \"" +
							name + "\"", errorPosition);
					}
					else
					{
						throw new XmlReaderException("The object or namespace \"" + parentNS.name +
							"\" already contains a child item called \"" + name + "\"", errorPosition);
					}
				}
				parentNS.childNamespaces.Add(name, this);
			}
		}

		public Namespace ParentNamespace
		{
			get { return parentNS; }
		}

		public string QualifiedName
		{
			get
			{
				if (parentNS != null)
				{
					string ret = parentNS.QualifiedName;
					if (name != null)
					{
						if (ret != "" && name != null)
							return ret + "." + name;
						else
							return name;
					}
					else
						return "";
				}
				else
					return name != null ? name : "";
			}
		}

		public CompilingObject GetObject(IList<string> nameParts, XmlPositionMarker errorPos)
		{
			Namespace curNs = this;
			for (int i = 0; i < nameParts.Count; i++)
			{
				Namespace childNs;
				if (curNs.childNamespaces.TryGetValue(nameParts[i], out childNs))
					curNs = childNs;
				else
					return null;
			}

			if (curNs.associatedObject == null)
			{
				string specifiedName = nameParts[0];
				for (int i = 1; i < nameParts.Count; i++)
					specifiedName += "." + nameParts[i];

				if (errorPos != null)
				{
					throw new XmlReaderException("The item \"" + specifiedName + "\" (\"" +
						curNs.QualifiedName + "\") isn't an object.", errorPos);
				}
				else
				{
					throw new Exception("The item \"" + specifiedName + "\" (\"" +
						curNs.QualifiedName + "\") isn't an object.");
				}
			}
			return curNs.associatedObject;
		}
	}
}